package edu.washington.cs.workflow.analysis.guistructure;

import java.awt.Component;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.MenuElement;
import javax.swing.text.JTextComponent;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.uispec4j.AbstractButton;
import org.uispec4j.MenuItem;
import org.uispec4j.Panel;
import org.uispec4j.Table;
import org.uispec4j.TextBox;
import org.uispec4j.Trigger;
import org.uispec4j.UIComponent;
import org.uispec4j.Window;
import org.uispec4j.finder.ComponentMatcher;
import org.uispec4j.interception.WindowHandler;
import org.uispec4j.interception.WindowInterceptor;

import edu.washington.cs.workflow.analysis.guistructure.uievents.MenuItemEvent;
import edu.washington.cs.workflow.analysis.guistructure.uievents.UIEvent;
import edu.washington.cs.workflow.analysis.guistructure.uievents.UIEventFactory;
import edu.washington.cs.workflow.analysis.guistructure.uievents.UIEvent.WINDOW;

import edu.washington.cs.workflow.util.Utils;


public class Spec4JUtils {
	
	public static WINDOW getTriggeredDialog(UIComponent uic, Trigger t) {
		WINDOW outWin = null;
		try {
//			Utils.debugPrintln("   cached disabled or miss the cache, wait for window..");
			//NOTE: the following code will run the window again, and 
			//may have undesired side effect
			Window w = WindowInterceptor.run(t);
			
			Utils.checkTrue(w != null);
			//when reaching at this point, it must be non-modal dialog here,
			//otherwise the run method will throw exception
			outWin = WINDOW.NONMODAL;
//			System.out.println(w.getDescription());
//			UIComponent[] colls = w.getUIComponents(ACTIVE_MATCHER);
//			System.out.println("size: " + colls.length + ", " + Utils.dumpArray(colls));
//			for(UIComponent coll : colls) {
//				System.out.println(coll.getLabel());
//			}
//			System.exit(1);
		} catch (Throwable e) {
			String message = e.getMessage();
			if(message == null) {
				outWin = WINDOW.NONE; //evaluating trigger leads to exception
			}else if(message.indexOf("No window was shown") != -1) {
				outWin = WINDOW.NONE;
			} else if(message.indexOf("it must be intercepted with a WindowHandler") != -1) {
				outWin = WINDOW.MODAL;
			} else if (message.indexOf("this window should be handled with WindowInterceptor") != -1) {
				outWin = WINDOW.MODAL;
			} else {
				//FIXME ugly hack
				if(message.indexOf("component must be showing") != - 1) {
					outWin = WINDOW.NONMODAL;
				} else if (message.indexOf("Row index out") != -1) {
					outWin = WINDOW.NONMODAL;
				} else if (e instanceof ClassCastException) {
					outWin = WINDOW.NONMODAL;
				}  else if (message.indexOf("defaultCloseOperation must be one") != -1) {
					outWin = WINDOW.MODAL;  //just for freemind
				}  else {
				    throw new Error(e);
				}
			}
		}
		
//		if(use_cache) {
//			addUIComponentToCache(uic, outWin);
//		}
		
		return outWin;
	}
	
	public static Panel getModalDialog(Trigger t) {
		final Panel[] tmpP = new Panel[1];
		WindowInterceptor.init(t).process(new WindowHandler() {
		    public Trigger process(Window dialog) {
		      tmpP[0] = dialog; //ugly hack here
		      return Trigger.DO_NOTHING;
		    }
		  }).run();
		Panel newPanel = tmpP[0];
		Utils.checkTrue(newPanel != null);
		return newPanel;
	}
	
	public static Panel getMatchedPanel(UIEvent event, Collection<Panel> activePanels) {
		Panel currentPanel = event.getCurrentPanel();
		return getMatchedPanel(currentPanel, activePanels);
	}
	
	public static Panel getMatchedPanel(Panel currentPanel, Collection<Panel> activePanels) {
		Utils.debugPrintln("Current panel: ");
		Utils.debugPrintln(currentPanel.getDescription());
		Utils.debugPrintln("Active panel num: " + activePanels.size());
		Utils.debugPrintln(System.identityHashCode(currentPanel));
		List<Panel> matchedPanels = new LinkedList<Panel>();
		int index = 1;
		for(Panel panel : activePanels) {
			Utils.debugPrintln("Active panel " + index + " : ");
			Utils.debugPrintln(panel.getDescription());
			Utils.debugPrintln(System.identityHashCode(panel));
			if(comparePanelByXML(currentPanel, panel)) {
				matchedPanels.add(panel);
			} else {
				Utils.debugPrintln(" not matched... at: " + index);
			}
			index++;
		}
		if(matchedPanels.isEmpty()) {
			return null;
		} else {
			Utils.checkTrue(matchedPanels.size() ==1, "matched panel num: " + matchedPanels.size()); //must be size 1
			return matchedPanels.get(0);
		}
	}
	
	static boolean comparePanelByXML(Panel p1, Panel p2) {
	    try {
		    return comparePanelByXMLInternal(p1, p2);
		} catch (DocumentException e) {
		    throw new Error(e);
		}
	}
	
	/**
	 * Here are some facts: uic may not on oldP. When finding the uic on newP, here are
	 * a few strategies:
	 * 
	 * It deals with menu separately
	 * 
	 * 1. just by comparing the description, if the same then it is matched
	 * 2. if multiple uic are matched, then use the location as another matching criteria
	 * 3. if location is not available on oldP, then fail
	 * */
	public static UIComponent getMatchedComponent(UIEvent event, Panel oldP, Panel newP) {
		//if event is an MenuEvent, just look up the menu in the new panel
		if(event instanceof MenuItemEvent) {
			Utils.checkTrue(newP instanceof Window, "Illegal type: " + newP.getClass());
			UIPos pos = ((MenuItemEvent)event).getUIComponentPos();
			Utils.checkTrue(pos instanceof UIMenuPos, "Illegal type: " + pos);
			String[] path = ((UIMenuPos)pos).getPaths();
			MenuItem item = Spec4JUtils.findMenuItem(newP, path);
			return item;
		}
		
		UIComponent uic = event.getUIComponent();
		UIComponent matchedUIC = getMatchedComponent(uic, oldP, newP);
		if(matchedUIC != null) {
			return matchedUIC;
		}
		
		UIPos p = event.getUIComponentPos();
		Utils.checkNotNull(p);
		UIComponent[] oldUIsOfSameType = oldP.getUIComponents(uic.getClass());
		UIComponent[] newUIsOfSameType = newP.getUIComponents(uic.getClass());
		//find component by locations in the old panel
		//XXX below are unsound
		if(oldUIsOfSameType.length == newUIsOfSameType.length) {
			int index = p.getIndex();
			Utils.checkTrue(oldUIsOfSameType.length > index);
			Utils.checkTrue(oldUIsOfSameType[index].getAwtComponent().getClass().equals(newUIsOfSameType[index].getAwtComponent().getClass()));
			return newUIsOfSameType[index];
		}
		Utils.debugPrintln("Not equal length: old: " + oldUIsOfSameType.length + ", new: " + newUIsOfSameType.length);
		return null;
	}
	private static UIComponent getMatchedComponent(UIComponent uic, Panel oldP, Panel newP) {
//		UIComponent[] oldUIs = oldP.getUIComponents(uic.getClass());
		UIComponent[] newUIs = newP.getUIComponents(uic.getClass());
		if(newUIs.length == 0) {
			Utils.debugPrintln("None matched for: " + uic.getDescription());
			return null;
		}
		if(newUIs.length == 1) {
			UIComponent matchedUIC = newUIs[0];
			Utils.checkNotNull(matchedUIC);
			Utils.checkTrue(matchedUIC.getAwtComponent().getClass().equals(uic.getAwtComponent().getClass()));
			return matchedUIC;
		}
		//find components with the same description
		List<UIComponent> uicsByDesc = getComponentsWithSameDescription(uic, newP);
		if(uicsByDesc.size() == 1) {
			return uicsByDesc.get(0);
		}
		Utils.debugPrintln("of the same disc : " + uic.getDescription() + " size: " + uicsByDesc.size());
		return null;
	}
	static List<UIComponent> getComponentsWithSameDescription(UIComponent uic, Panel panel) {
		List<UIComponent> list = new LinkedList<UIComponent>();
		for(UIComponent eachUIC : panel.getUIComponents(ComponentMatcher.ALL)) {
			if(eachUIC == null || !eachUIC.getAwtComponent().getClass().equals(uic.getAwtComponent().getClass())) {
//				if(eachUIC != null) {
//				    Utils.debugPrintln(eachUIC.getAwtComponent().getClass() + ", " + uic.getAwtComponent().getClass());
//				}
				continue;
			}
//			Utils.debugPrintln(" ==  " + eachUIC.getAwtComponent().getClass() + ", " + uic.getAwtComponent().getClass());
			if(eachUIC.getDescription().equals(uic.getDescription())) {
				list.add(eachUIC);
			}
		}
		return list;
	}
	static int findFirstCompIndexByComparingName(UIComponent uic, UIComponent[] uics) {
		for(int i = 0; i < uics.length; i++) {
			if(uics[i] == null) {
				continue;
			}
			if(uics[i].getName() == null) {
				continue;
			}
			if(uics[i].getName().equals(uic.getName())) {
				return i;
			}
		}
		return -1;
	}
	
	/**
	 * uic is a UI widget on the old panel
	 * */
	public static UIComponent getMatchedComponentByXML(UIComponent oldUIC, Panel oldP, Panel p) {
		return null;
	}
	
   static boolean comparePanelByXMLInternal(Panel p1, Panel p2) throws DocumentException {
	   String xml1 = p1.getDescription();
	   String xml2 = p2.getDescription();
	   Document doc1 = DocumentHelper.parseText(xml1);
	   Document doc2 = DocumentHelper.parseText(xml2);
	   
	   System.out.println("here is the xml 1:");
	   System.out.println(xml1);
	   System.out.println("here is the xml 2:");
	   System.out.println(xml2);
	   
	   Element root1 = doc1.getRootElement();
	   Element root2 = doc2.getRootElement();
	   
	   if(!root1.getName().equals(root2.getName())) {
		   Utils.debugPrintln("Root name not the same");
		   return false;
	   }
	   
	   if(root1.elements().size() != root2.elements().size()) {
		   Utils.debugPrintln("Size not the same: " + root1.elements().size() + ", v.s., " + root2.elements().size());
		   return false;
	   }
	   
	   //compare one by one
	   for(int index = 0; index < root1.elements().size(); index++) {
		   Element e1 = (Element) root1.elements().get(index);
		   Element e2 = (Element) root2.elements().get(index);
		   if(!e1.getName().equals(e2.getName())) {
			   Utils.debugPrintln("Element below root not the same.");
			   return false;
		   }
	   }
	   
	   return true;
   }
   
   public static Collection<UIEvent> allAvailableUIEvents(Panel panel) {
	   return getNonNullUIEvents(panel, false);
   }
   
   /**
    * It actually returns events randomly
    * */
   public static Collection<UIEvent> allNonNullRandomUIEvents(Panel panel) {
	   return getNonNullUIEvents(panel, true);
   }
   
   private static Collection<UIEvent> getNonNullUIEvents(Panel panel, boolean random) {
		//get all classes
		Collection<UIComponent> allActiveComponents = Spec4JUtils.getAllActiveComponents(Collections.singletonList(panel));
		Set<Class<? extends UIComponent>> componentClasses = new LinkedHashSet<Class<? extends UIComponent>>();
		for(UIComponent c : allActiveComponents) {
			if(c == null) {
				continue;
			}
			componentClasses.add(c.getClass());
		}
		//all UI event except for the menu
		Collection<UIEvent> uiEvents = new LinkedList<UIEvent>();
		for(Class<? extends UIComponent> clz : componentClasses) {
			if(clz.equals(MenuItem.class)) {
				//will process separately by the following code
				continue;
			}
			UIComponent[] uics = panel.getUIComponents(clz);
			for(int index = 0; index < uics.length; index++) {
				UIComponent uic = uics[index];
				if(uic == null) {
					continue;
				}
				if(!uic.isEnabled().isTrue()) {
					continue;
				}
				UIPos pos = UIPos.make(index);
				if(random) {
				    UIEvent event = UIEventFactory.createRandomEvent(uic, panel, pos);
				    if(event != null) {
					    uiEvents.add(event);
				    }
				} else {
					Collection<UIEvent> events = UIEventFactory.createAllEvents(uic, panel, pos);
					uiEvents.addAll(events);
				}
			}
		}
		
		//get all available menu events
		for(UIComponent uic : allActiveComponents) {
			if(uic == null || !uic.isEnabled().isTrue()) {
				continue;
			}
			if(uic instanceof MenuItem) {
				List<List<String>> pathlist = getAllSubMenuPaths((MenuItem)uic);
//				for(List<String> path : pathlist) {
//					Utils.debugPrintln(path);
//				}
				//FIXME it is likely that within the same level, multiple menu items
				//can have same text, for example, all entries use empty strings
				for(List<String> path : pathlist) {
					MenuItem menuItem = null;
					try {
					   menuItem = findMenuItem(panel, path);
					} catch (Throwable e) {
						//FIXME do not sth
						Utils.debugPrintln("Cannot find menu item: " + path + ". Missing or duplication.");
					}
					if(menuItem != null) {
						if(random) {
					        UIEvent event = UIEventFactory.createRandomEvent(menuItem, panel, UIMenuPos.make(path.toArray(new String[0])));
					        if(event != null) {
						        uiEvents.add(event);
					        }
						} else {
							Collection<UIEvent> events = UIEventFactory.createAllEvents(menuItem, panel, UIMenuPos.make(path.toArray(new String[0])));
							uiEvents.addAll(events);
						}
					}
				}
			}
		}
		
		return uiEvents;
	}
   
   private static String getMenuText(MenuItem item) {
	   Component comp = item.getAwtComponent();
	   if(comp instanceof JMenuItem) {
		   return ((JMenuItem)comp).getText();
	   }
	   throw new Error("Unsupported: " + comp);
   }
   
   public static List<MenuItem> findAllMenuItems(Panel panel, MenuItem topItem) {
	   List<MenuItem> menuItems = new LinkedList<MenuItem>();
	   
	   List<List<String>> pathlist = getAllSubMenuPaths(topItem);
	   for(List<String> path : pathlist) {
	       MenuItem menuItem = null;
		   try {
		       menuItem = findMenuItem(panel, path);
		   } catch (Throwable e) {
			   Utils.debugPrintln("Cannot find menu item: " + path + ". Missing or duplication.");
		   }
		   if(menuItem != null) {
			   menuItems.add(menuItem);
		   }
	   }
	   
	   return menuItems;
   }
   
   public static MenuItem findMenuItem(Panel panel, String[] paths) {
	   return findMenuItem(panel, Arrays.asList(paths));
   }
   
   public static MenuItem findMenuItem(Panel panel, List<String> path) {
	   Utils.checkTrue(panel instanceof Window, "type: " + panel);
	   Utils.checkNotNull(path);
	   Utils.checkTrue(!path.isEmpty());
	   
	   Window window = (Window)panel;
	   
	   MenuItem elem = window.getMenuBar().getMenu(path.get(0));
	   for(int i = 1; i < path.size(); i++) {
		   elem = elem.getSubMenu(path.get(i));
	   }
	   
	   return elem;
   }
   
   public static List<List<String>> getAllSubMenuPaths(MenuItem item) {
	   Component comp = item.getAwtComponent();
	   Utils.checkTrue(comp instanceof MenuElement, "Illegal type: " + item.getDescription());
	   MenuElement element = (MenuElement)comp;
	   
	   List<List<String>> initlist = new LinkedList<List<String>>();
	   List<List<String>> resultList = getAllSubMenuPaths(element, initlist);
	   
	   return resultList;
   }
   
   private static List<List<String>> getAllSubMenuPaths(MenuElement element, List<List<String>> list) {
	   Utils.checkNotNull(list);
	   if(list.isEmpty()) {
		   list.add(new LinkedList<String>());
	   }
	   if(element instanceof JMenuItem) {
		   String text = ((JMenuItem)element).getText();
		   for(List<String> l : list) {
			   l.add(text);
		   }
	   }
	   
	   List<List<String>> copylist = new LinkedList<List<String>>();
	   for(List<String> lstr : list) {
		   List<String> clst = new LinkedList<String>();
		   clst.addAll(lstr);
		   copylist.add(clst);
	   }
	   
	   MenuElement[] subElements = element.getSubElements();
	   if(subElements.length == 0) {
		   return list;
	   } else {
		   list.clear();
	   }
	   
	   for(MenuElement subElement : subElements) {
		   
		   List<List<String>> eachCopyList = new LinkedList<List<String>>();
		   for(List<String> lstr : copylist) {
			   List<String> clst = new LinkedList<String>();
			   clst.addAll(lstr);
			   eachCopyList.add(clst);
		   }
		   
		   List<List<String>> subList = getAllSubMenuPaths(subElement, eachCopyList);
		   list.addAll(subList);
	   }
	   return list;
   }
   
   public static List<String> getSubMenuText(MenuItem item) {
	   Component comp = item.getAwtComponent();
	   Utils.checkTrue(comp instanceof MenuElement, "Illegal type: " + item.getDescription());
	   MenuElement element = (MenuElement)comp;
	   List<String> subMenuText = getSubMenuText(element);
	   return subMenuText;
   }
   
   private static List<String> getSubMenuText(MenuElement element) {
	   List<String> subMenu = new LinkedList<String>();
       MenuElement[] subElements = element.getSubElements();
	   for(MenuElement subE : subElements) {
		   if(subE instanceof JMenuItem) {
			   subMenu.add(((JMenuItem)subE).getText());
		   } else if (subE instanceof JPopupMenu) {
			   subMenu.addAll(getSubMenuText(subE));
		   } else {
			   throw new Error("Illegal type: " + subE);
		   }
	   }
	   
	   return subMenu;
   }
   
	public static UIEvent nextRandomUIEvent(Panel panel) {
		Collection<UIEvent> allNonNulls = allNonNullRandomUIEvents(panel);
		if(allNonNulls.isEmpty()) {
			return null;
		} else {
			return Utils.randomElement(allNonNulls);
		}
	}
   
   public static void main(String[] args) throws DocumentException {
	   String text = "<window><table/><number/></window>";
	   Document d = DocumentHelper.parseText(text);
	   
	   Utils.debugPrintln(d.asXML());
	   Utils.debugPrintln(d.getRootElement());
	   Utils.debugPrintln(d.getRootElement().getName());
	   Utils.debugPrintln(d.getRootElement().elements().size());
	   for (@SuppressWarnings("rawtypes")Iterator i = d.getRootElement().elementIterator(); i.hasNext();) {
		   Element element = (Element) i.next();
		   // do something
		   Utils.debugPrintln("  " + element);
		   }
   }
	
   public static ComponentMatcher ACTIVE_MATCHER = new ComponentMatcher() {
	    public boolean matches(Component component) {
	        return component.isEnabled() && component.isVisible();
	      }
	    };
	    
	public static Collection<UIComponent> getAllActiveComponents(Collection<Panel> activePanels) {
		Collection<UIComponent> uics = new LinkedList<UIComponent>();
		for(Panel p : activePanels) {
			UIComponent[] comps = p.getUIComponents(ACTIVE_MATCHER);
			for(UIComponent comp : comps) {
				uics.add(comp);
			}
		}
		return uics;
	}
	
	public static Collection<UIComponent> getAllGUIElements(Collection<Panel> panels) {
		Collection<UIComponent> uics = new LinkedList<UIComponent>();
		for(Panel p : panels) {
			UIComponent[] comps = p.getUIComponents(ComponentMatcher.ALL);
			for(UIComponent comp : comps) {
				if(comp == null) {
					continue;
				}
				uics.add(comp);
				if(comp instanceof MenuItem) {
					List<MenuItem> menuItems = findAllMenuItems(p, (MenuItem)comp);
					uics.addAll(menuItems);
				}
			}
		}
		return uics;
	}
	
	public static boolean isQuitEvent(UIEvent event) {
		return event.toString().indexOf("Quit") != -1
		    || event.toString().indexOf("Exit") != -1;
	}
	
	public static String getLabel(UIComponent uic) {
		if(uic instanceof AbstractButton) {
			return ((AbstractButton)uic).getLabel();
		} else if (uic instanceof MenuItem) {
			return getMenuLabel((MenuItem)uic);
		} else if (uic instanceof TextBox) {
			TextBox box = (TextBox)uic;
			return getTextboxLabel(box);
		}
		return uic.getLabel();
	}
	
	public static String getMenuLabel(MenuItem item) {
		if(item.getAwtComponent() instanceof JMenuItem) {
			return ((JMenuItem)item.getAwtComponent()).getText();
		} else if(item.getAwtComponent() instanceof JPopupMenu) {
			return ((JPopupMenu)item.getAwtComponent()).getLabel();
		} else {
			return null;
		}
	}
	
	public static String getTextboxLabel(TextBox box) {
		JComponent comp = box.getAwtComponent();
		
		if(comp instanceof JLabel) {
			return ((JLabel)comp).getText();
		} else if (comp instanceof JTextComponent) {
			return ((JTextComponent)comp).getText();
		}
		
		return null;
	}
}
